<?php
/*
Mooha Server - SyncML Server written in PHP (http://mooha.net)
Copyright (C) 2009	Artem Bandur <artico.bandurini@gmail.com>
					Madcap BV (http://www.madcap.nl)

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA	02110-1301, USA.
*/

require_once('inc/WbxmlHelper.php');

interface MoohaServerInterface {

	public function log($str);

	public function getParam($name);
	public function getParams($names = array());
	public function getAllParams();
	public function getRequestHeaders();
	public function getResponseHeaders();
	public function getInput();
	public function getOutput();
	public function getRequest();
	public function getResponse();

	public function request($input = '', $contentType = '');
	public function headers();
	public function response();
	public function getMysql($config = array());
	public function addDatabase($userId, $dbName, $dbType, $dbUserName = '', $dbPassword = '');
	public function authenticateUser($userName, $userPassword, $digest = FALSE, $nonce = '', $imei = '');

}

class MoohaServer implements MoohaServerInterface {
	public $version = '0.2b'; // mooha server version
	private $config = array();
	private $params = array();
	private $input = '';
	private $output = '';
	private $request;
	private $response;
	private $client;
	private $server;
	private $statusCode = 200;
	private $noFinal = FALSE;
	private $wbxml = FALSE;
	private $conflicts = array();
	private $addons = array();
	private $syncmlVersion = '1.1';
	private $verCTs = array(
		'text/calendar' => '2.0',
		'text/x-vcalendar' => '1.0',
		'text/directory' => '3.0',
		'text/x-vcard' => '2.1',
		'text/plain' => '1.0',
		'text/x-vbookmark' => '1.0',
		'text/x-vmessage' => '1.0',
		'application/vnd.wap.mms-message' => '1.0',
		'application/vnd.omads-folder+xml' => '1.0'
	);
	private $syncTypes = array(
		200 => 1,	// two-way
		201 => 2,	// slow sync
		//202 => 3,	// one-way from client
		//203 => 4,	// refresh from client
		//204 => 5,	// one-way from server
		//205 => 6	// refresh from server
	);
	private $dbTypes = array('bookmarks', 'calendar', 'contacts', 'notes', 'tasks', 'sms', 'mms');
	private $autoCreateDatabase = TRUE;
	public $wbxmlHelper;
	public $forceFormat = ''; // if no content-type header was provided, treat request format as this value (XML or WBXML)
	
	function __construct($config = array()) {
		// collect params
		$this->params['protocol'] = @$_SERVER['SERVER_PROTOCOL'];
		$this->params['ssl'] = !(empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] == 'off');
		$this->params['port'] = @$_SERVER['SERVER_PORT'];
		$this->params['host'] = @$_SERVER['HTTP_HOST'];
		$this->params['path'] = @$_SERVER['REQUEST_URI'];
		$this->params['method'] = @$_SERVER['REQUEST_METHOD'];
		$this->params['contentType'] = @$_SERVER['CONTENT_TYPE'];
		$this->params['accept'] = @$_SERVER['HTTP_ACCEPT'];
		$this->params['charset'] = @$_SERVER['HTTP_ACCEPT_CHARSET'];
		$this->params['encoding'] = @$_SERVER['HTTP_ACCEPT_ENCODING'];
		$this->params['language'] = @$_SERVER['HTTP_ACCEPT_LANGUAGE'];
		$this->params['connection'] = @$_SERVER['HTTP_CONNECTION'];
		$this->params['referer'] = @$_SERVER['HTTP_REFERER'];
		$this->params['userAgent'] = @$_SERVER['HTTP_USER_AGENT'];
		$this->params['userIP'] = @$_SERVER['REMOTE_ADDR'];
		$this->params['userHost'] = @$_SERVER['REMOTE_HOST'];
		$this->params['userPort'] = @$_SERVER['REMOTE_PORT'];
		$this->params['userName'] = @$_SERVER['PHP_AUTH_USER'];
		$this->params['userPassword'] = @$_SERVER['PHP_AUTH_PW'];
		$this->params['authType'] = @$_SERVER['AUTH_TYPE'];
		$this->params['admin'] = @$_SERVER['SERVER_ADMIN'];
		$this->params['server'] = @$_SERVER['SERVER_SOFTWARE'];
		// mysql
		$this->mysql = $this->getMysql($config);
		if (!empty($config['forceFormat'])) {
			$this->forceFormat = $config['forceFormat'];
		}
		$this->config = $config;
	}
	
	public function getMysql($config = array()) {
		if (gettype($config) == 'string' && !empty($config)) {
			$u = parse_url($config);
			$config = array(
				'mysqlUser' => $u['user'],
				'mysqlPass' => $u['pass'],
				'mysqlHost' => $u['host'],
				'mysqlDb' => preg_replace('/^\//', '', $u['path']),
			);
		}
		else {
			if (empty($config)) {$config = $this->config;}
		}
		$mysql = mysql_connect($config['mysqlHost'], $config['mysqlUser'], $config['mysqlPass']);
		if ($mysql === FALSE) {
			throw new SyncmlException('Cannot run Mysql', 100);
		}
		if (!mysql_select_db($config['mysqlDb'], $mysql)) {
			throw new SyncmlException('Cannot select Mysql database', 100);
		}
		if (mysql_client_encoding($mysql) != 'utf8') {
			if (function_exists('mysql_set_charset')) {
				mysql_set_charset('utf8', $mysql);
			}
			else {
				mysql_query('SET NAMES utf8', $mysql);
				mysql_query('SET CHARACTER SET utf8', $mysql);
			}
		}
		return $mysql;
	}
	
	private function _mysqlQuery($query, $values = array()) {
		// parse table names (started with .)
		if (preg_match_all('/(\.mooha_[a-zA-Z0-9_]+)/', $query, $matches)) {
			$tables = array('mooha_anchors', 'mooha_dbs', 'mooha_items', 'mooha_map', 'mooha_sessions', 'mooha_users');
			$replaces = array();
			foreach ($matches[1] as $key => $match) {
				$name = preg_replace('/^\./', '', $match);
				$matches[1][$key] = '/([^a-zA-Z0-9_]+|^)\\' . $match . '([^a-zA-Z0-9_]+|$)/';
				$replaces[] = '${1}' . (in_array($name, $tables) ? ($this->config['mysqlPrefix'] . $name) : $match) . '${2}';
			}
			$query = preg_replace($matches[1], $replaces, $query);
		}
		// parse values (started with :)
		if (!empty($values) && preg_match_all('/(\:[a-zA-Z0-9_]+)/', $query, $matches)) {
			$replaces = array();
			foreach ($matches[1] as $key => $match) {
				$name = preg_replace('/^\:/', '', $match);
				$matches[1][$key] = '/([^a-zA-Z0-9_]+|^)\\' . $match . '([^a-zA-Z0-9_]+|$)/';
				if (array_key_exists($name, $values)) {
					$replaces[] = '${1}' . (is_int($values[$name]) ? '' : '\'') . mysql_real_escape_string($values[$name], $this->mysql) . (is_int($values[$name]) ? '' : '\'') . '${2}';
				}
				else {
					$replaces[] = '${1}' . $match . '${2}';
				}
			}
			$query = preg_replace($matches[1], $replaces, $query);
		}
		$res = mysql_query($query, $this->mysql);
		if ($res === FALSE) {
			throw new SyncmlException('Mysql query error: ' . mysql_error() . "\n query was: " . $query, 108);
		}
		return $res;
	}
	
	public function log($str) {
		@file_put_contents(dirname(dirname(__FILE__)) . '/mooha.log', date('[d-M-Y H:i:s] ') . $str . "\n", FILE_APPEND);
	}

/* Environment */
	
	public function getParam($name){
		return $this->params[$name];
	}
	
	public function getParams($names = array()){
		$output = array();
		if (empty($names) || !is_array($names)) {
			return $output;
		}
		foreach ($names as $name) {
			$output[$name] = $this->params[$name];
		}
		return $output;
	}
	
	public function getAllParams(){
		return $this->params;
	}
	
	public function getRequestHeaders(){
		return apache_request_headers();
	}
	
	public function getResponseHeaders(){
		$out = array();
		foreach (headers_list() as $header) {
			$pair = explode(':', $header);
			$out[trim($pair[0])] = trim($pair[1]);
		}
		return array_merge(apache_response_headers(), $out);
	}
	
	public function getInput(){
		return $this->input;
	}
	
	public function getOutput(){
		return $this->output;
	}
	
	public function getRequest(){
		return $this->request;
	}
	
	public function getResponse(){
		return $this->response;
	}

/* Process */
	
	public function request($input = '', $contentType = ''){
		$this->input = empty($input) ? file_get_contents('php://input') : $input;
		$contentType = empty($contentType) ? $this->params['contentType'] : $contentType;
		$decodedInput = '';
		// XML
		if ($this->forceFormat == 'XML' || $contentType == 'application/vnd.syncml+xml') {
			$decodedInput = $this->input;
		}
		// WBXML
		else if ($this->forceFormat == 'WBXML' || $contentType == 'application/vnd.syncml+wbxml') {
			$this->wbxmlHelper = new WbxmlHelper();
			$decodedInput = $this->wbxmlHelper->decode($this->input);
			$this->wbxml = TRUE;
			if (!WBXML_USE_EXTERNAL) { // PHP-based decoder has a bug with CDATA, so we need to overcome this manually
				if (preg_match_all('/\<Data\>(.+?)\<\/Data\>/s', $decodedInput, $matches)) {
					foreach ($matches[1] as $key => $item) {
						if (preg_match('/[^a-zA-Z0-9\*\-\+\/\_\!\.\:\=]/', $item) && @simplexml_load_string($item) === FALSE) {
							$matches[1][$key] = '<![CDATA[' . $item . ']]>';
						}
						$matches[1][$key] = '<Data>' . $matches[1][$key] . '</Data>';
					}
					$decodedInput = str_replace($matches[0], $matches[1], $decodedInput);
				}
			}
		}
		// non-SyncML
		else {
			throw new SyncmlException('Request is non-SyncML', 105);
		}
		if (!empty($decodedInput)) {
			libxml_use_internal_errors(true);
			$this->request = simplexml_load_string($decodedInput);
		}
		// Unreadable XML
		if ($this->request === FALSE) {
			try {
				$libxmlErrors = libxml_get_errors();
				if (!empty($libxmlErrors)) {
					$libxmlErrorLevels = array(
						LIBXML_ERR_WARNING => 'Warning',
						LIBXML_ERR_ERROR => 'Error',
						LIBXML_ERR_FATAL => 'Fatal error'
					);
					$err = '';
					foreach ($libxmlErrors as $libxmlError) {
						$err .= "\n...LIBXML " . $libxmlErrorLevels[$libxmlError->level] . ' ' . $libxmlError->code . ': ' . trim($libxmlError->message) . ' on line ' . $libxmlError->line . '(' . $libxmlError->column . ')' . (empty($libxmlError->file) ? '' : (' in file ' . $libxmlError->file)) . (empty($decodedInput) ? '' : "\n...input:\n" . $decodedInput . "\n");
					}
					throw new SyncmlException('Request XML couldn\'t be parsed due to some libxml errors: ' . $err, 106);
				}
				libxml_clean_errors();
			}
			catch (Exception $e) {
				throw new SyncmlException('Request is unreadable', 106, $e);
			}
		}
	}
	
	public function headers() {
		header('Content-Type: ' . ($this->wbxml ? 'application/vnd.syncml+wbxml' : 'application/vnd.syncml+xml'));
		header('Content-Length: ' . strlen($this->output));
		header('Accept-Charset: UTF-8');
	}
	
	public function response() {
		if (empty($this->request)) {
			throw new SyncmlException('Request is empty', 107);
		}
		$DTDs = array(
			'1.0' => 'http://www.openmobilealliance.org/tech/affiliates/syncml/syncml_represent_v10_20001207.dtd',
			'1.1' => 'http://www.openmobilealliance.org/tech/affiliates/syncml/syncml_represent_v11_20020215.dtd',
			'1.2' => 'http://www.openmobilealliance.org/Tech/DTD/OMA-TS-SyncML_RepPro_DTD-V1_2-20050303-D.dtd'
		);
		$this->syncmlVersion = (string)$this->request->SyncHdr->VerDTD;
		$doctype = '<!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML ' . $this->syncmlVersion . '//EN" "' . $DTDs[$this->syncmlVersion] . '">';
		$this->response = simplexml_load_string($doctype . '<SyncML></SyncML>');
		$this->response->addAttribute('xmlns', 'SYNCML:SYNCML' . $this->syncmlVersion); 
		$this->cmdID = 0;
		$this->msgID = 1;
		$this->client = $this->request->SyncHdr->Source->LocURI;
		$this->server = $this->request->SyncHdr->Target->LocURI;
		$this->maxMsgSize = min($this->_parseSize(ini_get('upload_max_filesize')), $this->_parseSize(ini_get('post_max_size')));
		$this->maxObjSize = round($this->_parseSize(ini_get('memory_limit')) * .9);
		// session
		$sid = md5($this->client . $this->request->SyncHdr->SessionID . $this->getParam('userIP'));
		if (session_id() != $sid) {
		 @session_destroy();
		}
		session_set_save_handler('moohaSessionOpen', 'moohaSessionClose', 'moohaSessionRead', 'moohaSessionWrite', 'moohaSessionDestroy', 'moohaSessionGc');
		session_id($sid);
		@session_start();
		// message ID counter
		if (isset($_SESSION['moohaMsgID'])) {
			$this->msgID = ++$_SESSION['moohaMsgID'];
		}
		else {
			$_SESSION['moohaMsgID'] = $this->msgID;
		}
		// build SyncHdr
		$this->_appendChild($this->_buildSyncHdr(), $this->response);
		// build SyncBody
		$this->response->addChild('SyncBody');
		// alert 222 for unfinished sync
		if (!isset($this->request->SyncBody->Final) && empty($_SESSION['moohaExcess'])) {
			$alert = $this->_buildNode('Alert', array(
				'CmdID' => ++$this->cmdID,
				'Data' => 222
			));
			$alert->addChild('Item')->addChild('Target')->addChild('LocURI', $this->server);
			$alert->Item->addChild('Source')->addChild('LocURI', $this->client);
			$this->_appendChild($alert, $this->response->SyncBody);
			$this->noFinal = TRUE;
		}
		// status for SyncHdr
		$this->_appendChild($this->_buildNode('Status', array(
			'CmdID' => ++$this->cmdID,
			'MsgRef' => $this->msgID,
			'CmdRef' => 0,
			'Cmd' => 'SyncHdr',
			'TargetRef' => $this->server,
			'SourceRef' => $this->client
		)), $this->response->SyncBody);
		// authentication
		$this->response->SyncBody->Status->addChild('Data', $this->_processAuthentication());
		// add LocName
		if (!empty($_SESSION['moohaUserName'])) {
			$this->response->SyncHdr->Target->addChild('LocName', $_SESSION['moohaUserName']);
		}
		// supply each SyncBody item
		foreach ($this->request->SyncBody->children() as $bodyItem) {
			$bodyItemName = $bodyItem->getName();
			$this->statusCode = (empty($_SESSION['moohaUserId']) && $bodyItemName != 'Get' && $bodyItemName != 'Put') ? 401 : 200;
			// Results for Get
			if ($bodyItemName == 'Get' && !empty($_SESSION['moohaUserId'])) {
				$this->addons[] = $this->_buildResults($bodyItem);
			}
			// no response required
			if ($bodyItemName == 'Status' || $bodyItemName == 'Final' || isset($bodyItem->NoResp)) {
				if($bodyItem->Cmd == "Sync") {
					$dbName = $bodyItem->SourceRef;	
				}

				// If we tried to send a replace cmd and the clientItemId doesn't exist then we need to delete the item
				if($bodyItem->Cmd == "Replace" && strpos($bodyItem->Item->Data, "Err") !== false)	{
					$this->_deleteItem($_SESSION['userId'], $dbName, $bodyItem->TargetRef, time());
				}
				continue;		
		}
			// other commands that needs status
			else {
				$statusItem = $this->_appendChild($this->_buildNode('Status', array(
					'CmdID' => ++$this->cmdID,
					'MsgRef' => $this->msgID,
					'CmdRef' => $bodyItem->CmdID,
					'Cmd' => $bodyItemName,
				)), $this->response->SyncBody);
				// if user authenticated
				if (!empty($_SESSION['moohaUserId'])) {
					if (!empty($bodyItem->Item->Target)) {
						$statusItem->addChild('TargetRef', $bodyItem->Item->Target->LocURI);
					}
					if (!empty($bodyItem->Item->Source)) {
						$statusItem->addChild('SourceRef', $bodyItem->Item->Source->LocURI);
					}
					if (!empty($bodyItem->Target)) {
						$statusItem->addChild('TargetRef', $bodyItem->Target->LocURI);
					}
					if (!empty($bodyItem->Source)) {
						$statusItem->addChild('SourceRef', $bodyItem->Source->LocURI);
					}
					if ($bodyItemName == 'Alert') {
						if ($bodyItem->Data == 222) {
							$this->_buildSync($bodyItem);
						}
						else {
							$statusAlertItem = $this->_buildAlert($bodyItem);
						}
					}
					else if ($bodyItemName == 'Sync') {
						$this->_buildSync($bodyItem);
					}
					else if ($bodyItemName == 'Map') {
						$this->_mapItems($bodyItem);
					}
				}
				// if user not authenticated
				else {
					if ($bodyItemName == 'Get') {
						$statusItem->addChild('TargetRef', $bodyItem->Item->Target->LocURI);
					}
					else if ($bodyItemName == 'Put') {
						$statusItem->addChild('SourceRef', $bodyItem->Item->Source->LocURI);
					}
				}
				$statusItem->addChild('Data', $this->statusCode);
				if ($bodyItemName == 'Alert' && !empty($statusAlertItem)) {
					$this->_appendChild($statusAlertItem, $statusItem);
				}
			}
		}
		// append additional items
		foreach ($this->addons as $addon) {
			$this->_appendChild($addon, $this->response->SyncBody);
		}
		// final
		if (!$this->noFinal) {
			$this->response->SyncBody->addChild('Final');
		}
		// output
		$this->output = $this->wbxml ? $this->wbxmlHelper->encode($this->response->asXML()) : $this->response->asXML();
		// end session
		if (/*$this->statusCode == 401 || */count($this->response->SyncBody->children()) == 2) {
			session_destroy();
		}
	}

/* Anchors */
	
	private function _getDatabaseAnchor($userId, $dbName) {
		$query = 'SELECT MAX(item_anchor) FROM .mooha_items JOIN .mooha_dbs USING(db_id) WHERE user_id = :user_id AND db_name = :db_name';
		$res = $this->_mysqlQuery($query, array(
			'user_id' => $userId,
			'db_name' => $dbName
		));
		$result = mysql_num_rows($res) ? mysql_result($res, 0) : 0;
		return empty($result) ? '' : $result;
	}
	
	private function _getClientAnchor($userId, $dbName) {
		$query = 'SELECT client_anchor FROM .mooha_anchors JOIN .mooha_dbs USING(db_id) WHERE user_id = :user_id AND db_name = :db_name AND client = :client';
		$res = $this->_mysqlQuery($query, array(
			'user_id' => $userId,
			'db_name' => $dbName,
			'client' => $this->client
		));
		return mysql_num_rows($res) ? (int)mysql_result($res, 0) : FALSE;
	}
	
	private function _getServerAnchor($userId, $dbName) {
		$query = 'SELECT server_anchor FROM .mooha_anchors JOIN .mooha_dbs USING(db_id) WHERE user_id = :user_id AND db_name = :db_name AND client = :client';
		$res = $this->_mysqlQuery($query, array(
			'user_id' => $userId,
			'db_name' => $dbName,
			'client' => $this->client
		));
		return mysql_num_rows($res) ? mysql_result($res, 0) : 0;
	}
	
	private function _resetServerAnchor($userId, $dbName) {
		$query = 'UPDATE .mooha_anchors JOIN .mooha_dbs USING(db_id) SET server_anchor = 0 WHERE user_id = :user_id AND db_name = :db_name';
		$res = $this->_mysqlQuery($query, array(
			'user_id' => $userId,
			'db_name' => $dbName
		));
		return $res;
	}
	
	private function _createAnchor() {
		return time();
	}
	
/* Databases */
	
	public function addDatabase($userId, $dbName, $dbType, $dbUserName = '', $dbPassword = '') {
		$query = 'INSERT INTO .mooha_dbs VALUES(:user_id, \'\', :db_type, :db_name, \'\', :db_username, :db_password)';
		if (!$this->_mysqlQuery($query, array(
			'user_id' => $userId,
			'db_name' => $dbName,
			'db_type' => $dbType,
			'db_username' => $dbUserName,
			'db_password' => $dbPassword
		))) {
			throw new SyncmlException('Cannot add database', 139);
		}
		return mysql_insert_id($this->mysql);
	}
	
	private function _getDatabases($userId) {
		$out = array();
		$query = 'SELECT db_type, db_name FROM .mooha_dbs WHERE user_id = :user_id';
		$res = $this->_mysqlQuery($query, array(
			'user_id' => $userId
		));
		while ($row = mysql_fetch_assoc($res)) {
			$out[$row['db_name']] = $row['db_type'];
		}
		return $out;
	}
	
	private function _getDatabaseIdByName($userId, $dbName) {
		$query = 'SELECT db_id FROM .mooha_dbs WHERE user_id = :user_id AND db_name = :db_name';
		$res = $this->_mysqlQuery($query, array(
			'user_id' => $userId,
			'db_name' => $dbName
		));
		if (mysql_num_rows($res)) {
			return mysql_result($res, 0);
		}
		else if ($this->autoCreateDatabase && in_array(strtolower($dbName), $this->dbTypes)) {
			return $this->addDatabase($userId, $dbName, strtolower($dbName));
		}
		else {
			return 0;
		}
	}
	
	private function _getChangedItems($userId, $dbName, $serverAnchor) {
		$out = array();
		$query = 'SELECT item_id, item_type, item_content, item_deleted FROM .mooha_items JOIN .mooha_dbs USING(db_id) WHERE user_id = :user_id AND db_name = :db_name AND item_anchor > :server_anchor';
		$res = $this->_mysqlQuery($query, array(
			'user_id' => $userId,
			'db_name' => $dbName,
			'server_anchor' => $serverAnchor
		));
		if (mysql_num_rows($res)) {
			while ($res_arr = mysql_fetch_assoc($res)) {
				$out[] = array(
					'id' => $res_arr['item_id'],
					'type' => $res_arr['item_type'],
					'content' => $res_arr['item_content'],
					'deleted' => (bool)$res_arr['item_deleted'],
					'clientId' => $this->_getClientItemId($userId, $dbName, $res_arr['item_id'])
				);
			}
		}
		return $out;
	}
	
	private function _getClientItemId($userId, $dbName, $itemId) {
		$query = 'SELECT client_item_id FROM .mooha_map JOIN .mooha_dbs USING(user_id) WHERE user_id = :user_id AND db_name = :db_name AND client = :client AND item_id = :item_id';
		$res = $this->_mysqlQuery($query, array(
			'user_id' => $userId,
			'db_name' => $dbName,
			'client' => $this->client,
			'item_id' => $itemId
		));
		return mysql_num_rows($res) ? mysql_result($res, 0) : 0;
	}
	
/* Items */
	
	private function _addItem($userId, $dbName, $clientItemId, $anchor, $type, $content) {
		$dbId = $this->_getDatabaseIdByName($userId, $dbName);
		$query = 'INSERT INTO .mooha_items VALUES(:db_id, \'\', :anchor, :type, :content, 0)';
		if (!$this->_mysqlQuery($query, array(
			'db_id' => $dbId,
			'anchor' => $anchor,
			'type' => $type,
			'content' => $content
		))) {
			throw new SyncmlException('Cannot add item', 134);
		}
		$itemId = mysql_insert_id($this->mysql);
		$query = 'INSERT INTO .mooha_map VALUES(:user_id, :db_id, :client, :client_item_id, :item_id)';
		if (!$this->_mysqlQuery($query, array(
			'user_id' => $userId,
			'db_id' => $dbId,
			'client' => $this->client,
			'client_item_id' => $clientItemId,
			'item_id' => $itemId
		))) {
			throw new SyncmlException('Cannot add map for item', 135);
		}
		return TRUE;
	}
	
	private function _replaceItem($userId, $dbName, $clientItemId, $anchor, $type, $content) {
		$dbId = $this->_getDatabaseIdByName($userId, $dbName);
		$query = 'SELECT item_id FROM .mooha_map WHERE user_id = :user_id AND db_id = :db_id AND client = :client AND client_item_id = :client_item_id';
		$res = $this->_mysqlQuery($query, array(
			'user_id' => $userId,
			'db_id' => $dbId,
			'client' => $this->client,
			'client_item_id' => $clientItemId
		));
		if (mysql_num_rows($res)) {
			$itemId = mysql_result($res, 0);
			$query = 'UPDATE .mooha_items SET item_anchor = :anchor, item_type = :type, item_content = :content WHERE item_id = :item_id';
			if (!$this->_mysqlQuery($query, array(
			'anchor' => $anchor,
			'type' => $type,
			'content' => $content,
			'item_id' => $itemId
		))) {
				throw new SyncmlException('Cannot replace item', 136);
			}
			return FALSE;
		}
		else {
			$this->_addItem($userId, $dbName, $clientItemId, $anchor, $type, $content);
			return TRUE;
		}
	}
	
	private function _deleteItem($userId, $dbName, $clientItemId, $anchor) {
		$dbId = $this->_getDatabaseIdByName($userId, $dbName);
		$query = 'SELECT item_id FROM .mooha_map WHERE user_id = :user_id AND db_id = :db_id AND client = :client AND client_item_id = :client_item_id';
		$res = $this->_mysqlQuery($query, array(
			'user_id' => $userId,
			'db_id' => $dbId,
			'client' => $this->client,
			'client_item_id' => $clientItemId
		));
		if (!mysql_num_rows($res)) {
			// item isn't exists
			return FALSE;
		}
		$itemId = mysql_result($res, 0);
		$query = 'SELECT COUNT(item_id) FROM .mooha_map WHERE item_id = :item_id';
		$res = $this->_mysqlQuery($query, array(
			'item_id' => $itemId
		));
		if (mysql_result($res, 0) > 1) {
			// soft delete
			$query = 'UPDATE .mooha_items SET item_anchor = :anchor, item_deleted = 1 WHERE item_id = :item_id';
		}
		else {
			// hard delete
			$query = 'DELETE FROM .mooha_items WHERE item_id = :item_id';
		}
		if (!$this->_mysqlQuery($query, array(
			'anchor' => $anchor,
			'item_id' => $itemId
		))) {
			throw new SyncmlException('Cannot delete item', 137);
		}
		// delete item mapping
		$query = 'DELETE FROM .mooha_map WHERE user_id = :user_id AND db_id = :db_id AND client = :client AND client_item_id = :client_item_id';
		if (!$this->_mysqlQuery($query, array(
			'user_id' => $userId,
			'db_id' => $dbId,
			'client' => $this->client,
			'client_item_id' => $clientItemId
		))) {
			throw new SyncmlException('Cannot delete map for item', 138);
		}
		return TRUE;
	}
	
	private function _mapItem($userId, $dbName, $clientItemId, $itemId) {
		$dbId = $this->_getDatabaseIdByName($userId, $dbName);
		$query = 'SELECT item_id FROM .mooha_map WHERE user_id = :user_id AND db_id = :db_id AND client = :client AND client_item_id = :client_item_id';
		$res = $this->_mysqlQuery($query, array(
			'user_id' => $userId,
			'db_id' => $dbId,
			'client' => $this->client,
			'client_item_id' => $clientItemId
		));
		if (mysql_num_rows($res)) {
			if (mysql_result($res, 0) != $itemId) {
				$query = 'UPDATE .mooha_map SET item_id = :item_id WHERE user_id = :user_id AND db_id = :db_id AND client = :client AND client_item_id = :client_item_id';
			}
		}
		else {
			$query = 'INSERT INTO .mooha_map VALUES(:user_id, :db_id, :client, :client_item_id, :item_id)';
		}
		if (!$this->_mysqlQuery($query, array(
			'user_id' => $userId,
			'db_id' => $dbId,
			'client' => $this->client,
			'client_item_id' => $clientItemId,
			'item_id' => $itemId
		))) {
			throw new SyncmlException('Cannot map item', 135);
		}
		return TRUE;
	}
	
	private function _mapItems($bodyItem) {
		foreach ($bodyItem->children() as $mapItem) {
			if ($mapItem->getName() != 'MapItem') {continue;}
			$dbName = $this->_cleanDbName($bodyItem->Target->LocURI);
			if (isset($_SESSION['moohaMap_' . $dbName . '_' . $mapItem->Target->LocURI])) {
				$this->_mapItem(
					$_SESSION['moohaUserId'],
					$dbName,
					$mapItem->Source->LocURI,
					$_SESSION['moohaMap_' . $dbName . '_' . $mapItem->Target->LocURI]
				);
			}
		}
	}
	
/* Sync */
	
	private function _clientSync($bodyItem) {
		$save = FALSE;
		$syncDatabase = $this->_cleanDbName((string)$bodyItem->Target->LocURI);
		foreach ($bodyItem->children() as $syncItem) {
			$syncItemName = $syncItem->getName();
			if (in_array($syncItemName, array('Add', 'Replace', 'Delete', 'Copy', 'Atomic', 'Sequence'))) {
						
		foreach($syncItem->children() as $item) {
			if($item->getName() == 'Item') {
					$syncItemLocURI = $item->Source->LocURI;
					if (empty($_SESSION['moohaUserId'])) {
						$syncItemStatus = 401;
					}
					else if (isset($item->MoreData)) {
						$this->noFinal = TRUE;
						$syncItemStatus = 213;
						if (array_key_exists('moohaChunk_' . $syncItemLocURI, $_SESSION)) {
							$_SESSION['moohaChunk_' . $syncItemLocURI] .= (string)$item->Data;
						}
						else {
							$_SESSION['moohaChunk_' . $syncItemLocURI] = (string)$item->Data;
						}
					}
					else {
						$syncItemStatus = 200;
						// make data modifications
						switch ($syncItemName) {
							 case 'Add':
								 $this->_addItem(
									$_SESSION['moohaUserId'],
									$syncDatabase,
									$syncItemLocURI,
									$_SESSION['moohaServerAnchor_' . $syncDatabase],
									$syncItem->Meta->Type,
									(array_key_exists('moohaChunk_' . $syncItemLocURI, $_SESSION) ? $_SESSION['moohaChunk_' . $syncItemLocURI] : '')
									 . $item->Data
								);
								$syncItemStatus = 201;
							break;
							case 'Replace':
								if ($this->_replaceItem(
									$_SESSION['moohaUserId'],
									$syncDatabase,
									$syncItemLocURI,
									$_SESSION['moohaServerAnchor_' . $syncDatabase],
									$syncItem->Meta->Type,
									(array_key_exists('moohaChunk_' . $syncItemLocURI, $_SESSION) ? $_SESSION['moohaChunk_' . $syncItemLocURI] : '')
									 . $item->Data
								)) {
									$syncItemStatus = 201;
								}
								else if (in_array($syncItemLocURI, $this->conflicts)) {
									// conflict resolved, client winning
									$syncItemStatus = 208;
								}
							break;
							case 'Delete':
								if (!$this->_deleteItem(
									$_SESSION['moohaUserId'],
									$syncDatabase,
									$syncItemLocURI,
									$_SESSION['moohaServerAnchor_' . $syncDatabase]
								)) {
									$syncItemStatus = 211;
								}
							break;
						}
					}
					// status for each Sync command
					$this->_appendChild($this->_buildNode('Status', array(
						'CmdID' => ++$this->cmdID,
						'MsgRef' => $this->msgID,
						'CmdRef' => $syncItem->CmdID,
						'Cmd' => $syncItemName,
						'SourceRef' => $syncItemLocURI,
						'Data' => $syncItemStatus
					)), $this->response->SyncBody);
					// save this non-empty sync
					$save = TRUE;
			}
		}
			}
		}
		return $save;
	}
	
	private function _serverSync($bodyItem) {
		$save = FALSE;
		$bodyItemName = $bodyItem->getName();
		$syncDatabase = $this->_cleanDbName(($bodyItemName == 'Alert' && !empty($_SESSION['moohaExcess'])) ? $_SESSION['moohaExcess']['target'] : (string)$bodyItem->Target->LocURI);
		// check if server db was changed since last sync
		$serverAnchor = $this->_getServerAnchor($_SESSION['moohaUserId'], $syncDatabase);
		$databaseAnchor = $this->_getDatabaseAnchor($_SESSION['moohaUserId'], $syncDatabase);
		$slowSync = isset($_SESSION['moohaAlertCode_' . $syncDatabase]) && $_SESSION['moohaAlertCode_' . $syncDatabase] == 201;
		if ($slowSync || ($serverAnchor != $databaseAnchor)) {
			$sync = $this->_buildNode('Sync', array(
				'CmdID' => ++$this->cmdID
			));
			$syncSource = ($bodyItemName == 'Alert' && !empty($_SESSION['moohaExcess'])) ? $_SESSION['moohaExcess']['source'] : (string)$bodyItem->Source->LocURI;
			$sync->addChild('Target')->addChild('LocURI', $syncSource);
		$sync->addChild('Source')->addChild('LocURI', $syncDatabase);
			$sync->addChild('Meta')->addChild('MaxObjSize', $this->maxObjSize)->addAttribute('xmlns', 'syncml:metinf');
			$changedItems = $this->_getChangedItems($_SESSION['moohaUserId'], $syncDatabase, $serverAnchor);
		// remove softly deleted items which were not mapped with this client
		$trueChangedItems = array();
		foreach($changedItems as $item) {
			if($item['clientId'] === 0 && $item['deleted']) {
				continue;
		}
		else {
			$trueChangedItems[] = $item;
		}
		}		
			$sync->addChild('NumberOfChanges', count($trueChangedItems));
			$tempId = 0;
			// get items deleted by the client to exclude them from server sync
			$deleted = array();
			$replaced = array();
			if ($bodyItemName != 'Alert') {
				foreach ($bodyItem->children() as $syncItem) {
					$syncItemName = $syncItem->getName();
					if ($syncItemName == 'Delete') {
					 foreach($syncItem->Item as $deletedItem) {
						 $deleted[] = $deletedItem->Source->LocURI;
			 }
					}
					else if ($syncItem->getName() == 'Replace') {
						foreach($syncItem->Item as $replacedItem) {
				$replaced[] = $replacedItem->Source->LocURI;	
			}
					}
				}
			}
			$contentLength = 0;
			$excess = '';
			$passed = array();
			foreach ($trueChangedItems as $item) {
				if ($bodyItemName == 'Alert' && !empty($_SESSION['moohaExcess']) && in_array($item['id'], $_SESSION['moohaExcess']['passed'])) {
					continue;
				}
				if (in_array($item['clientId'], $deleted)) {
					continue;
				}
				if (in_array($item['clientId'], $replaced)) {
					$this->conflicts[] = $item['clientId'];
			if($item['deleted']) $commandName = 'Delete';
			else	continue;
				}
				if ($item['deleted']) {
					$commandName = 'Delete';
				}
				else if (empty($item['clientId'])) {
					$commandName = 'Add';
					$tempId++;
					$_SESSION['moohaMap_' . $syncDatabase . '_' . $tempId] = $item['id'];
				}
				else {
					$commandName = 'Replace';
				}
				if (empty($excess)) {
					$command = $this->_buildNode($commandName, array(
						'CmdID' => ++$this->cmdID,
						'Item' => ''
					));
					if ($commandName == 'Add') {
						$command->Item->addChild('Source')->addChild('LocURI', $tempId);
					}
					else {
						$command->Item->addChild('Target')->addChild('LocURI', $item['clientId']);
					}
					$command->Item->addChild('Meta')->addChild('Type', $item['type'])->addAttribute('xmlns', 'syncml:metinf');
					if (!empty($this->verCTs[$item['type']])) {
						$command->Item->Meta->addChild('Version', $this->verCTs[$item['type']])->addAttribute('xmlns', 'syncml:metinf');
					}
					if ($commandName != 'Delete') {
						if ($bodyItemName == 'Alert' && !empty($_SESSION['moohaExcess']) && $_SESSION['moohaExcess']['item']['id'] == $item['id']) {
							$item['content'] = $_SESSION['moohaExcess']['item']['content'];
						}
						$itemSize = strlen($item['content']);
						$contentLength += $itemSize;
						if (!empty($this->request->SyncHdr->Meta->MaxMsgSize) && ($contentLength / (int)$this->request->SyncHdr->Meta->MaxMsgSize > ($this->wbxml ? .5 : .25))) {
							$trim = floor((int)$this->request->SyncHdr->Meta->MaxMsgSize * ($this->wbxml ? .5 : .25));
							if (strlen($item['content']) > $trim) {
								$excess = substr($item['content'], $trim);
								$item['content'] = substr($item['content'], 0, $trim);
							}
							else {
								$excess = TRUE;
							}
						}
						$node = dom_import_simplexml($command->Item->addChild('Data'));
						$node->appendChild($node->ownerDocument->createCDATASection($item['content']));
						if (!empty($excess)) {
							if (empty($_SESSION['moohaExcess']) || (!empty($_SESSION['moohaExcess']['item']['id']) && $_SESSION['moohaExcess']['item']['id'] != $item['id'])) {
								$command->Item->Meta->addChild('Size', $itemSize)->addAttribute('xmlns', 'syncml:metinf');
							}
							if ($excess !== TRUE) {
								$command->Item->addChild('MoreData');
							}
							$this->noFinal = TRUE;
							if (!empty($_SESSION['moohaExcess'])) {
								$passed = array_merge($_SESSION['moohaExcess']['passed'], $passed);
							}
							$_SESSION['moohaExcess'] = array(
								'target' => $syncDatabase,
								'source' => $syncSource,
								'item' => array(
									'id' => ($excess === TRUE ? 0 : $item['id']),
									'content' => $excess
								),
								'passed' => $passed
							);
						}
						else {
							$passed[] = $item['id'];
							$this->noFinal = FALSE;
						}
					}
					if ($excess !== TRUE) {
						$this->_appendChild($command, $sync);
					}
					if (!empty($excess)) break;	// stop sync
				}
			}
			$save = empty($excess);
			$this->addons[] = $sync;
		}
		return $save;
	}
	
	private function _saveSync($userId, $dbName, $serverAnchor, $clientAnchor) {
		$dbId = $this->_getDatabaseIdByName($userId, $dbName);
		if (!$dbId) {
			return false;
		}
		$serverAnchor = empty($serverAnchor) ? 0 : $serverAnchor;
		$clientAnchor = empty($clientAnchor) ? 0 : $clientAnchor;
		try {
			if ($this->_getClientAnchor($userId, $dbName, $this->client) !== FALSE) {
				$query = 'UPDATE .mooha_anchors SET client_anchor = :client_anchor, server_anchor = :server_anchor WHERE db_id = :db_id AND client = :client';
				if (!$this->_mysqlQuery($query, array(
					'client_anchor' => $clientAnchor,
					'server_anchor' => $serverAnchor,
					'db_id' => $dbId,
					'client' => $this->client
				))) {
					throw new SyncmlException('Cannot update anchor: ' . mysql_error(), 132);
				}
			}
			else {
				$query = 'INSERT INTO .mooha_anchors VALUES(:db_id, :client, :client_anchor, :server_anchor)';
				if (!$this->_mysqlQuery($query, array(
					'client_anchor' => $clientAnchor,
					'server_anchor' => $serverAnchor,
					'db_id' => $dbId,
					'client' => $this->client
				))) {
					throw new SyncmlException('Cannot insert anchor: ' . mysql_error(), 131);
				}
			}
		}
		catch (Exception $e) {
			throw new SyncmlException('Cannot save sync', 130, $e);
		}
		return TRUE;
	}
	
/* Build Nodes */
	
	private function _appendChild($node, $parent) {
		$parent = dom_import_simplexml($parent);
		return simplexml_import_dom($parent->appendChild($parent->ownerDocument->importNode(dom_import_simplexml($node), TRUE)));
	}
	
	private function _insertBefore($node, $refNode, $parent) {
		$parent = dom_import_simplexml($parent);
		return simplexml_import_dom($parent->insertBefore($parent->ownerDocument->importNode(dom_import_simplexml($node), TRUE), $parent->ownerDocument->importNode(dom_import_simplexml($refNode), TRUE)));
	}
	
	private function _buildNode($name, $children = array()) {
		$node = simplexml_load_string('<' . $name . '></' . $name . '>');
		foreach ($children as $name => $value) {
			$node->addChild($name, $value);
		}
		return $node;
	}
	
	private function _buildSyncCap() {
		$syncCap = $this->_buildNode('SyncCap');
		foreach ($this->syncTypes as $code => $type) {
			$syncCap->addChild('SyncType', $type);
		}
		return $syncCap;
	}
	
	private function _buildResults($bodyItem, $nodeType = 'Results') {		// nodeType can also be 'Put'
		$results = $this->_buildNode($nodeType, array(
			'CmdID' => ++$this->cmdID,
			'MsgRef' => $this->msgID,
			'CmdRef' => $bodyItem->CmdID
		));
		$results->addChild('Meta')->addChild('Type', $bodyItem->Meta->Type)->addAttribute('xmlns', 'syncml:metinf');
		$results->addChild('Item')->addChild('Source')->addChild('LocURI', './devinf' . str_replace('.', '', $this->syncmlVersion));
		$devInf = $results->Item->addChild('Data')->addChild('DevInf');
		$devInf->addAttribute('xmlns', 'syncml:devinf');
		$devInf->addChild('VerDTD', $this->syncmlVersion);
		$devInf->addChild('Man', 'Mooha');
		$devInf->addChild('DevID', $this->params['host'] . $this->params['path']);
		$devInf->addChild('Mod', 'Mooha SyncML Server ' . $this->version);
		$devInf->addChild('OEM', 'PHP');
		$devInf->addChild('DevTyp', 'server');
		$devInf->addChild('SupportLargeObjs');
		$devInf->addChild('SupportNumberOfChanges');
		if (!empty($_SESSION['moohaUserId'])) {
			foreach ($this->_getDatabases($_SESSION['moohaUserId']) as $dbName => $dbType) {
				$dataStore = $devInf->addChild('DataStore');
				$dataStore->addChild('SourceRef', $dbName);
				switch ($dbType) {
					case 'calendar':
					case 'tasks':
						$rxPref = $dataStore->addChild('Rx-Pref');
						$rxPref->addChild('CTType', 'text/calendar');
						$rxPref->addChild('VerCT', $this->verCTs['text/calendar']);
						$rx = $dataStore->addChild('Rx');
						$rx->addChild('CTType', 'text/x-vcalendar');
						$rx->addChild('VerCT', $this->verCTs['text/x-vcalendar']);
						$txPref = $dataStore->addChild('Tx-Pref');
						$txPref->addChild('CTType', 'text/calendar');
						$txPref->addChild('VerCT', $this->verCTs['text/calendar']);
						$tx = $dataStore->addChild('Tx');
						$tx->addChild('CTType', 'text/x-vcalendar');
						$tx->addChild('VerCT', $this->verCTs['text/x-vcalendar']);
						$this->_appendChild($this->_buildSyncCap(), $dataStore);
					break;
					case 'contacts':
						$rxPref = $dataStore->addChild('Rx-Pref');
						$rxPref->addChild('CTType', 'text/directory');
						$rxPref->addChild('VerCT', $this->verCTs['text/directory']);
						$rx = $dataStore->addChild('Rx');
						$rx->addChild('CTType', 'text/x-vcard');
						$rx->addChild('VerCT', $this->verCTs['text/x-vcard']);
						$txPref = $dataStore->addChild('Tx-Pref');
						$txPref->addChild('CTType', 'text/directory');
						$txPref->addChild('VerCT', $this->verCTs['text/directory']);
						$tx = $dataStore->addChild('Tx');
						$tx->addChild('CTType', 'text/x-vcard');
						$tx->addChild('VerCT', $this->verCTs['text/x-vcard']);
						$this->_appendChild($this->_buildSyncCap(), $dataStore);
					break;
					case 'bookmarks':
						$rxPref = $dataStore->addChild('Rx-Pref');
						$rxPref->addChild('CTType', 'text/x-vbookmark');
						$rxPref->addChild('VerCT', $this->verCTs['text/x-vbookmark']);
						$rx = $dataStore->addChild('Rx');
						$rx->addChild('CTType', 'text/plain');
						$rx->addChild('VerCT', $this->verCTs['text/plain']);
						$txPref = $dataStore->addChild('Tx-Pref');
						$txPref->addChild('CTType', 'text/x-vbookmark');
						$txPref->addChild('VerCT', $this->verCTs['text/x-vbookmark']);
						$tx = $dataStore->addChild('Tx');
						$tx->addChild('CTType', 'text/plain');
						$tx->addChild('VerCT', $this->verCTs['text/plain']);
						$this->_appendChild($this->_buildSyncCap(), $dataStore);
					break;
					case 'notes':
						$rxPref = $dataStore->addChild('Rx-Pref');
						$rxPref->addChild('CTType', 'text/plain');
						$rxPref->addChild('VerCT', $this->verCTs['text/plain']);
						$txPref = $dataStore->addChild('Tx-Pref');
						$txPref->addChild('CTType', 'text/plain');
						$txPref->addChild('VerCT', $this->verCTs['text/plain']);
						$this->_appendChild($this->_buildSyncCap(), $dataStore);
					break;
					case 'sms':
						$rxPref = $dataStore->addChild('Rx-Pref');
						$rxPref->addChild('CTType', 'text/x-vmessage');
						$rxPref->addChild('VerCT', $this->verCTs['text/x-vmessage']);
						$rx = $dataStore->addChild('Rx');
						$rx->addChild('CTType', 'application/vnd.omads-folder+xml');
						$rx->addChild('VerCT', $this->verCTs['application/vnd.omads-folder+xml']);
						$txPref = $dataStore->addChild('Tx-Pref');
						$txPref->addChild('CTType', 'text/x-vmessage');
						$txPref->addChild('VerCT', $this->verCTs['text/x-vmessage']);
						$tx = $dataStore->addChild('Tx');
						$tx->addChild('CTType', 'application/vnd.omads-folder+xml');
						$tx->addChild('VerCT', $this->verCTs['application/vnd.omads-folder+xml']);
						$this->_appendChild($this->_buildSyncCap(), $dataStore);
					break;
					case 'mms':
						$rxPref = $dataStore->addChild('Rx-Pref');
						$rxPref->addChild('CTType', 'application/vnd.wap.mms-message');
						$rxPref->addChild('VerCT', $this->verCTs['application/vnd.wap.mms-message']);
						$rx = $dataStore->addChild('Rx');
						$rx->addChild('CTType', 'application/vnd.omads-folder+xml');
						$rx->addChild('VerCT', $this->verCTs['application/vnd.omads-folder+xml']);
						$txPref = $dataStore->addChild('Tx-Pref');
						$txPref->addChild('CTType', 'application/vnd.wap.mms-message');
						$txPref->addChild('VerCT', $this->verCTs['application/vnd.wap.mms-message']);
						$tx = $dataStore->addChild('Tx');
						$tx->addChild('CTType', 'application/vnd.omads-folder+xml');
						$tx->addChild('VerCT', $this->verCTs['application/vnd.omads-folder+xml']);
						$this->_appendChild($this->_buildSyncCap(), $dataStore);
					break;
				}
			}
		}
		return $results;
	}
	
	private function _buildAlert($bodyItem) {
		$alertDatabase = $this->_cleanDbName((string)$bodyItem->Item->Target->LocURI);
		// check if db exists
		if ($this->_getDatabaseIdByName($_SESSION['moohaUserId'], $alertDatabase)) {
			// save client anchor
			$_SESSION['moohaClientAnchor_' . $alertDatabase] = (string)$bodyItem->Item->Meta->Anchor->Next;
			// pass the anchors back
			$statusItem = $this->_buildNode('Item');
			$statusItem->addChild('Data')->addChild('Anchor');//->addChild('Last', $bodyItem->Item->Meta->Anchor->Last);
			$statusItem->Data->Anchor->addChild('Next', $bodyItem->Item->Meta->Anchor->Next);
			$statusItem->Data->Anchor->addAttribute('xmlns', 'syncml:metinf');
			// create next server anchor
			$_SESSION['moohaServerAnchor_' . $alertDatabase] = $this->_createAnchor();
			// response alert
			$alert = $this->_appendChild($this->_buildNode('Alert', array(
				'CmdID' => ++$this->cmdID,
			)), $this->response->SyncBody);
			// check fresh
			if ((int)$bodyItem->Item->Meta->Anchor->Last !== $this->_getClientAnchor($_SESSION['moohaUserId'], $alertDatabase)) {
				// slow sync
				$this->_resetServerAnchor($_SESSION['moohaUserId'], $alertDatabase);
				$alertCode = 201;
				if ($bodyItem->Data != 201) {
					// refresh required
					$this->statusCode = 508;
				}
			}
			else {
				// two-way sync
				$alertCode = 200;
			}
			$alert->addChild('Data', $alertCode);
			// save alert code for server sync
			$_SESSION['moohaAlertCode_' . $alertDatabase] = $alertCode;
			// alert item
			$alert->addChild('Item')->addChild('Target')->addChild('LocURI', $bodyItem->Item->Source->LocURI);
			$alert->Item->addChild('Source')->addChild('LocURI', $bodyItem->Item->Target->LocURI);
			$alert->Item->addChild('Meta')->addChild('Anchor');
			$alert->Item->Meta->Anchor->addChild('Last', $this->_getDatabaseAnchor($_SESSION['moohaUserId'], $alertDatabase));
			$alert->Item->Meta->Anchor->addChild('Next', $_SESSION['moohaServerAnchor_' . $alertDatabase]);
			$alert->Item->Meta->Anchor->addAttribute('xmlns', 'syncml:metinf');
			$alert->Item->Meta->addChild('MaxObjSize', $this->maxObjSize)->addAttribute('xmlns', 'syncml:metinf');
		}
		else {
			// item not found
			$this->statusCode = 404;
		}
		return isset($statusItem) ? $statusItem : NULL;
	}
	
	private function _buildSyncHdr() {
		$syncHdr = $this->_buildNode('SyncHdr', array(
			'VerDTD' => $this->syncmlVersion,
			'VerProto' => 'SyncML/' . $this->syncmlVersion,
			'SessionID' => $this->request->SyncHdr->SessionID,
			'MsgID' => $this->msgID,
			'Target' => '',
			'Source' => '',
			'RespURI' => $this->server,
			'Meta' => ''
		));
		$syncHdr->Target->addChild('LocURI', $this->client);
		$syncHdr->Source->addChild('LocURI', $this->server);
		$syncHdr->Meta->addChild('MaxMsgSize', $this->maxMsgSize)->addAttribute('xmlns', 'syncml:metinf');
		$syncHdr->Meta->addChild('MaxObjSize', $this->maxObjSize)->addAttribute('xmlns', 'syncml:metinf');
		return $syncHdr;
	}
	
	private function _buildSync($bodyItem) {
		// reset conflicts
		$this->conflicts = array();
		if ($bodyItem->getName() == 'Alert') {
			// sync from server if has not finished
			$serverSync = $this->_serverSync($bodyItem);
			$clientSync = FALSE;
			$syncDatabase = $_SESSION['moohaExcess']['target'];
		}
		else {
			// sync from server if client has finished
			$serverSync = isset($this->request->SyncBody->Final) ? $this->_serverSync($bodyItem) : FALSE;
			// sync from client
			$clientSync = $this->_clientSync($bodyItem);
			$syncDatabase = $this->_cleanDbName($bodyItem->Target->LocURI);
		}
		// save sync
		if (($serverSync || $clientSync) && !empty($_SESSION['moohaUserId'])) {
			$this->_saveSync(
				$_SESSION['moohaUserId'],
				$syncDatabase,
				$_SESSION['moohaServerAnchor_' . $syncDatabase],
				$_SESSION['moohaClientAnchor_' . $syncDatabase]
			);
		}
	}
	
/* Authentication */
	
	public function authenticateUser($userName, $userPassword, $digest = FALSE, $nonce = '', $imei = '') {
		if (empty($imei)) {
			$query = 'SELECT user_id, user_password FROM .mooha_users WHERE user_name = :user_name';
			$res = $this->_mysqlQuery($query, array(
				'user_name' => $userName
			));
		}
		else {
			$query = 'SELECT user_id FROM .mooha_users WHERE imei = :imei';
			$res = $this->_mysqlQuery($query, array(
				'imei' => md5($imei)
			));
		}
		if (mysql_num_rows($res)) {
			if (empty($imei)) {
				$realUserPassword = mysql_result($res, 0, 1);
				if ($digest && $userPassword == $this->_md5digest($realUserPassword, $nonce)) {
					// granted digest auth
					return mysql_result($res, 0, 0);
				}
				else if (md5($userName . ':' . $userPassword) == $realUserPassword) {
					// granted basic auth
					return mysql_result($res, 0, 0);
				}
				else {
					// auth failed
					return 0;
				}
			}
			else {
				return mysql_result($res, 0, 0);
			}
		}
		else {
			// userName or IMEI doesn't exists
			return 0;
		}
	}
	
	public function getUserName($userId) {
		$query = 'SELECT user_name FROM .mooha_users WHERE user_id = :user_id';
		$res = $this->_mysqlQuery($query, array(
			'user_id' => $userId
		));
		return mysql_num_rows($res) ? mysql_result($res, 0, 0) : '';
	}
	
	public function getUserImei($userId) {
		$query = 'SELECT imei FROM .mooha_users WHERE user_id = :user_id';
		$res = $this->_mysqlQuery($query, array(
			'user_id' => $userId
		));
		return mysql_num_rows($res) ? mysql_result($res, 0, 0) : '';
	}
	
	public function setUserImei($userId, $imei) {
		$query = 'UPDATE .mooha_users SET imei = :imei WHERE user_id = :user_id';
		$res = $this->_mysqlQuery($query, array(
			'user_id' => $userId,
			'imei' => $imei
		));
		return mysql_num_rows($res) ? mysql_result($res, 0, 0) : '';
	}
	
	public function addUser($userId, $userName, $userPassword, $imei = '') {
		$query = 'INSERT INTO .mooha_users VALUES(:user_id, :user_name, :user_password, :imei)';
		return $res = $this->_mysqlQuery($query, array(
			'user_id' => $userId,
			'user_name' => $userName,
			'user_password' => $userPassword,
			'imei' => $imei
		));
	}
	
	public function updateUser($userId, $userName, $userPassword, $imei = '') {
		$updatePassword = $userPassword !== NULL;
		$query = 'UPDATE .mooha_users SET user_name = :user_name' . ($updatePassword ? ', user_password = :user_password' : '') . ', imei = :imei WHERE user_id = :user_id';
		return $res = $this->_mysqlQuery($query, array(
			'user_id' => $userId,
			'user_name' => $userName,
			'user_password' => $userPassword,
			'imei' => $imei
		));
	}
	
	public function removeUser($userId) {
		$query = 'DELETE FROM .mooha_users WHERE user_id = :user_id';
		return $res = $this->_mysqlQuery($query, array(
			'user_id' => $userId
		));
	}

	private function _md5digest($userPassword, $nonce = '') {		// userPassword expected as md5(user:pass)
		return base64_encode(pack('H*', md5(base64_encode(pack('H*', $userPassword)) . ':' . $nonce)));
	}
	
	private function _processAuthentication() {
		$status = 200;
		if (empty($_SESSION['moohaUserId'])) {
			$authType = '';
			if (empty($this->request->SyncHdr->Cred)) {
				if (!empty($_SESSION['moohaAuthExpected']) && preg_match('/^IMEI\:\d+/', $this->request->SyncHdr->Source->LocURI)) {
					$authType = 'imei';
				}
			}
			else if (empty($this->config['authDigest'])) {
				if ($this->request->SyncHdr->Cred->Meta->Type == 'syncml:auth-basic') {
					$authType = 'basic';
				}
			}
			else if ($this->request->SyncHdr->Cred->Meta->Type == 'syncml:auth-md5') {
				$authType = 'digest';
			}
			if (empty($authType) || ($authType == 'digest' && empty($_SESSION['moohaAuthExpected']))) {
				// credentials missing or digest auth comes unexpectedly with an old nonce
				$status = empty($this->request->SyncHdr->Cred) ? 407 : 401;
				$_SESSION['moohaAuthExpected'] = TRUE;
				$this->response->SyncBody->Status->addChild('Chal')->addChild('Meta');
				if (empty($this->config['authDigest'])) {
					$this->response->SyncBody->Status->Chal->Meta->addChild('Type', 'syncml:auth-basic')->addAttribute('xmlns', 'syncml:metinf');
				}
				else {
					$this->response->SyncBody->Status->Chal->Meta->addChild('Type', 'syncml:auth-md5')->addAttribute('xmlns', 'syncml:metinf');
					$_SESSION['moohaAuthNonce'] = substr(md5(uniqid('', TRUE)), rand(0, 24), 16); // from 8 to 16 random symbols
					$this->response->SyncBody->Status->Chal->Meta->addChild('NextNonce', base64_encode($_SESSION['moohaAuthNonce']))->addAttribute('xmlns', 'syncml:metinf');
				}
				$this->response->SyncBody->Status->Chal->Meta->addChild('Format', 'b64')->addAttribute('xmlns', 'syncml:metinf');
			}
			else {
				$status = 401;
				switch ($authType) {
					case 'basic':
						$pair = explode(':', base64_decode($this->request->SyncHdr->Cred->Data));
						if ($userId = $this->authenticateUser($pair[0], $pair[1])) {
							$_SESSION['moohaUserId'] = $userId;
							$_SESSION['moohaUserName'] = $pair[0];
							$status = 212;
						}
					break;
					case 'digest':
						if ($userId = $this->authenticateUser($this->request->SyncHdr->Source->LocName, $this->request->SyncHdr->Cred->Data, TRUE, @$_SESSION['moohaAuthNonce'])) {
							$_SESSION['moohaUserId'] = $userId;
							$_SESSION['moohaUserName'] = (string)$this->request->SyncHdr->Source->LocName;
							$status = 212;
						}
					break;
					case 'imei':
						if ($userId = $this->authenticateUser('', '', FALSE, '', (string)$this->request->SyncHdr->Source->LocURI)) {
							$_SESSION['moohaUserId'] = $userId;
							$_SESSION['moohaUserName'] = $this->getUserName($userId);
							$status = 212;
						}
					break;
				}
			}
		}
		return $status;
	}
	
	private function _cleanDbName($dbName) {
		return preg_replace('/^\.\//', '', $dbName);	// nokia syncml client unexpectedly adds ./ at the start of db name
	}
	
	private function _parseSize($size) {
		$unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
		$size = preg_replace('/[^0-9\.]/', '', $size);
		if ($unit) {
			return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
		}
		else {
			return round($size);
		}
	}
}



// extending the interface
interface MoohaServerSimpleInterface extends MoohaServerInterface {
	// add a new public method
	public function process();
}

// extending the class
class MoohaServerSimple extends MoohaServer implements MoohaServerSimpleInterface {
	// implement a new public method
	public function process(){
		$this->request();
		$this->response();
		$this->headers();
		return $this->getOutput();
	}
}



// custom session handling

function moohaSessionOpen($path, $name) {
	return TRUE;
}

function moohaSessionClose() {
	return TRUE;
}

function moohaSessionRead($id) {
	register_shutdown_function('session_write_close');
	global $db_url;
	global $db_prefix;
	$mysql = MoohaServer::getMysql($db_url);
	$query = 'SELECT session_value FROM ' . $db_prefix . 'mooha_sessions WHERE session_id = \'' . mysql_real_escape_string($id, $mysql) . '\'';
	$res = mysql_query($query, $mysql);
	if (!$res) {
		throw new SyncmlException('Cannot run Mysql while sessionRead: ' . mysql_error(), 100);
	}
	return mysql_num_rows($res) ? mysql_result($res, 0, 0) : '';
}

function moohaSessionWrite($id, $value) {
	global $db_url;
	global $db_prefix;
	$mysql = MoohaServer::getMysql($db_url);
	$query = 'REPLACE INTO ' . $db_prefix . 'mooha_sessions VALUES (\'' . mysql_real_escape_string($id, $mysql) . '\', \'' . mysql_real_escape_string($value, $mysql) . '\', ' . time() . ')';
	$res = mysql_query($query, $mysql);
	if (!$res) {
		throw new SyncmlException('Cannot run Mysql while sessionWrite', 100);
	}
	return $res;
}

function moohaSessionDestroy($id) {
	global $db_url;
	global $db_prefix;
	$mysql = MoohaServer::getMysql($db_url);
	$query = 'DELETE FROM ' . $db_prefix . 'mooha_sessions WHERE session_id = \'' . mysql_real_escape_string($id, $mysql) . '\'';
	$res = mysql_query($query, $mysql);
	if (!$res) {
		throw new SyncmlException('Cannot run Mysql while sessionDestroy', 100);
	}
	return $res;
}

function moohaSessionGc($life) {
	global $db_url;
	global $db_prefix;
	$mysql = MoohaServer::getMysql($db_url);
	$query = 'DELETE FROM ' . $db_prefix . 'mooha_sessions WHERE session_time < ' . (time() - (int)mysql_real_escape_string($life, $mysql));
	$res = mysql_query($query, $mysql);
	if (!$res) {
		throw new SyncmlException('Cannot run Mysql while sessionGc', 100);
	}
	return $res;
}